#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
    Part of Seagull - The Pyhon Pages engine
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    renderer:
      Looks up the pages from the page providers, tries to render
      the requested page, and sends the result to a given output.
"""
__author__ = 'Jonathan Slenders <jonathan at slenders dot be>'
__license__ = 'GNU LGPL'

__all__ = ('Renderer', 'PostProcessException', 'PostProcessException',
	'FilterException', 'RenderException', 'OutputBufferException')

import xml.dom.minidom
import sys
import os
import datetime
import traceback
import threading
import StringIO

from configuration import SystemConfig
import page_lookup
import server_interface

from utils import xml_escape
import renderer_input
import magic
import page_providers

################################################################
# Multithreaded Output stream
################################################################

class MultiThreadedStdOut(object):
	"""
	Helper class to make different threads use different stdouts
	This is a singleton because a new import of this module would
	otherwise overwrite the created stdout.
	"""
	__instance = None

	class _MultiThreadedStdOut(object):
		def __init__(self):
			self.__stdout_objects = { }

		def register_stdout(self, object):
			""" Register this stdout object for the current thread """
			id = threading.currentThread()

			if self.__stdout_objects.has_key(id):
				self.__stdout_objects[id].append(object)
			else:
				self.__stdout_objects[id] = [ object ]

		def delete_stdout(self):
			""" UnRegister the stdout for the current thread """
			id = threading.currentThread()

			if len(self.__stdout_objects[id]) > 1:
				self.__stdout_objects[id].pop()
			else:
				del self.__stdout_objects[id]

		def write(self, arg):
			""" Write to the stdout of the current thread """
			id = threading.currentThread()

			if self.__stdout_objects.has_key(id):
				self.__stdout_objects[id][-1:][0].write(arg)
			else:
				sys.__stdout__.write(arg)

		def the_real_stdout_doesnt_have_this_function(self):
			"""
			Use this function to check whether or not stdout
			has been replaced. Using global variables is no
			option because they're reintitialised for each
			import. (That's what it looked like)
			"""
			pass


	def __init__(self):
		if MultiThreadedStdOut.__instance is None:
			try:
				# Stdout replaced in the past, use that one!
				sys.stdout.the_real_stdout_doesnt_have_this_function()
				MultiThreadedStdOut.__instance = sys.stdout
			except:
				# Otherwise, create a new one
				MultiThreadedStdOut.__instance = MultiThreadedStdOut._MultiThreadedStdOut()

		self.__dict__['_MultiThreadedStdOut__config'] = MultiThreadedStdOut.__instance

	def __getattr__(self, attr):
		""" Delegate access to implementation """
		return getattr(MultiThreadedStdOut.__instance, attr)

	def __setattr__(self, attr, value):
		""" Delegate access to implementation """
		return setattr(MultiThreadedStdOut.__instance, attr, value)


# Route all stdout through this class
_stdout = MultiThreadedStdOut()
sys.stdout = _stdout


################################################################
# Output buffer stub, result of the renderer goes through this class
################################################################

class OutputBuffer(object):
	"""
	Results from the page handler are going through this output buffer
	"""
	def __init__(self):
		self.__writing_body = False # Status

	def headers_have_been_send(self):
		return self.__writing_body

	def write_headers(self, headers, status_code=404):
		"""
		headers: dictionary of key/value pairs
		status code: integer
		"""
		if self.__writing_body:
			raise OutputBufferException('Headers already sent')
		else:
			self._write_headers_body(headers, status_code)

		self.__writing_body = True

	def write(self, string):
		if len(string) > 0:
			if self.__writing_body:
				# We output to a terminal/socket, so
				# convert to str
				if string.__class__.__name__ == 'unicode':
					self._write_content_body(string.encode('utf-8'))
				else:
					self._write_content_body(string)
			else:
				raise OutputBufferException('Please sent headers first')

	def flush(self):
		"""
		Send all data in the outputbuffer to the client.
		A chucked output for instance will do some buffering
		and shall implement this.
		"""
		if self.__writing_body:
			self._flush_body()

	def _write_headers_body(self, headers, status_code):
		raise NotImplementedError()

	def _write_content_body(self, string):
		raise NotImplementedError()
	
	def _flush_body(self):
		pass

################################################################
# Renderer
################################################################

class Renderer(object):
	"""
	Executes the pages.
	"""
	class StreamBuffer(object):
		"""
		stdout replacement that buffers
		output in a string.
		"""
		def __init__(self):
			self.__buffer = []

		def write(self, data):
			# We remember code in UTF-8 encoded str
			if data.__class__.__name__ == 'unicode':
				self.__buffer.append(data.encode('utf-8'))
			else:
				self.__buffer.append(data)

		def retrieve_data(self):
			return ''.join(self.__buffer)

	def __init__(self, render_input, output_buffer):
		try:
			self.__render_input = render_input
			self.__output_buffer = output_buffer
			self.__server = None

			# Log request
			for logger in SystemConfig().loggers:
				logger.log_request(render_input.client_addr,
					render_input.requested_page, render_input.query_string)


			# Find corresponding page
			page = page_lookup.PageLookup().resolve_page(render_input.requested_page)

			# In case of binary file: stream
			if isinstance(page, page_providers.BinaryPage):
				self.__execute_binary_file(page, output_buffer)

			# In case of Python Page, process
			elif isinstance(page, page_providers.PythonPage):
				# Create 'Server' environment
				self.__server = server_interface.ServerInterface(render_input, output_buffer)
				self.__server.url_suffix = page.url_suffix
				self.__server.page_path = page.path

				try:
					# Initialize page
					page_object = page_lookup.PageLookup().initialize_page(page, self.__server)

					# Execute
					self.__execute_page(page_object, output_buffer)

					# Delete 'server' environment (will store the session)
					self.__server.dispose()
				except object, e:
					self.__server.dispose()
					raise e
			else:
				raise Exception('Unknown Page object')

		except AccessDeniedException, e:
			self.__handle_access_denied_error(render_input.requested_page, str(e))

		except page_lookup.PageNotFoundException, e:
			self.__handle_page_not_found_error(render_input.requested_page)

		except PostProcessException, e:
			self.__handle_runtime_error( {
					'xhtml_output': str(e.xhtml_output),
					'xhtml_parse_error_message': str(e.error_message),
					'xhtml_parse_error_lineno': str(e.lineno),
					'xhtml_parse_error_offset': str(e.offset) })

		except Exception, e:
			sys.__stderr__.write('-> Exception: %s\n' % str(e))
			# TODO: error logging
			sys.__stderr__.write(traceback.format_exc())

			self.__handle_runtime_error( {'traceback': traceback.format_exc()} )

		except:
			sys.__stderr__.write('-> Exception: ???\n')
			self.__handle_runtime_error( {'traceback': traceback.format_exc()} )

	def __handle_runtime_error(self, params):
		"""
		Renders the 500 Internal Server Error page to the output
		"""
		error_page = SystemConfig().error_pages[500]

		if not 'traceback' in params:
			params['traceback'] = ''

		# Read the tracelog
		if self.__server:
			params['tracelog'] = u'\n'.join([m.message for m in self.__server.get_trace()])

		# Parameter dictionary
		params['original_page'] = self.__render_input.requested_page
		params.update(error_page.parameters)

		# Render this 500 error page if headers haven't been send, and one error page is defined
		if (not self.__output_buffer.headers_have_been_send() and
					(error_page and self.__render_input.requested_page != error_page.page_path)):

			input = renderer_input.SimpleRenderInput(SystemConfig().error_pages[500].page_path, params)
			input.cookie = self.__render_input.cookie
			Renderer(input, self.__output_buffer)
		else:
			# Otherwise, render the default error page
			self.__write_error_500_message(params)

	def __handle_access_denied_error(self, original_page, message):
		if (401 in SystemConfig().error_pages and
				self.__render_input.requested_page != SystemConfig().error_pages[401].page_path):

			input = renderer_input.SimpleRenderInput(SystemConfig().error_pages[401].page_path, 
					{ 'original_page': original_page, 'message' : message })
			input.cookie = self.__render_input.cookie
			Renderer(input, self.__output_buffer)
		else:
			raise Exception('No 401 error page defined in system config')

	def __handle_page_not_found_error(self, original_page):
		if (404 in SystemConfig().error_pages and
				self.__render_input.requested_page != SystemConfig().error_pages[404].page_path):

			input = renderer_input.SimpleRenderInput(SystemConfig().error_pages[404].page_path, 
					{ 'original_page' : original_page})
			input.cookie = self.__render_input.cookie
			Renderer(input, self.__output_buffer)
		else:
			raise Exception('No 404 error page defined in system config')

	def __write_error_500_message(self, params):
		"""
		When no error page is found, are the configured error page does not render,
		send this default error message.
		"""
		complete_message = False
		if not self.__output_buffer.headers_have_been_send():
			self.__output_buffer.write_headers( { 'Content-Type' : 'text/html' }, 500)
			complete_message = True

		if complete_message:
			self.__output_buffer.write('<html><head><title>500 Internal Server Error</title></head></body>')

		self.__output_buffer.write('\n\n\n<h1>500 Internal Server Error</h1>')

		if 'xhtml_parse_error_message' in params:
			self.__output_buffer.write('<p><strong>%s</strong></p>' % xml_escape(str(params['xhtml_parse_error_message'])))
		if 'tracelog' in params:
			self.__output_buffer.write('<h2>Tracelog</h2>')
			self.__output_buffer.write('<pre>%s</pre>' % xml_escape(params['tracelog']))
		if 'traceback' in params:
			self.__output_buffer.write('<h2>Traceback</h2>')
			self.__output_buffer.write('<pre>%s</pre>' % xml_escape(params['traceback']))

		if complete_message:
			self.__output_buffer.write('</body></html>')

	def __execute_binary_file(self, binary_page, output_buffer):
		"""
		Stream binary file to output
		"""
		# Headers
		output_buffer.write_headers({
			# Content type through magic
			'Content-Type': magic.content_type_from_filename(binary_page.path),

			# Cache control (100 days)
			'Expires': (datetime.datetime.now() + datetime.timedelta(+100)).
					strftime(self.__render_input.http_header_time_format),
			'Cache-Control': 'public',

			}, 200)

		# Send file contents
		binary_page.stream_data_content(output_buffer)

	def __execute_page(self, page_object, output_buffer):
		"""
		Load this page, evaluate, and return the result
		Path is relative to the /page folder
		"""
		# Check whether we this page is external accessible
		if not page_object.external:
			raise AccessDeniedException('Page is not external accessible ' + 
					'(This is not a stand-alone page and probably part of another page)')

		# Set output mode
		if page_object.output_mode:
			self.__server.output_mode = page_object.output_mode

		# Set Content-Type
		if page_object.content_type:
			self.__server.headers['Content-Type'] = page_object.content_type

		# Select stream depending on page settings
		# NOTE: that the raw_output var will be set after initialisation of the page class
		if self.__server.output_mode == 'raw':
			stream = output_buffer

		elif self.__server.output_mode in ('xhtml', 'atom'):
			stream = self.StreamBuffer()

		else:
			raise RenderException('Unknown output mode "%s"' % self.__server.output_mode)

		# Replace output stream
		_stdout.register_stdout(stream)

		try:
			page_object.execute()
			page_object.dispose()

		except server_interface.ExitPageException:
			# ExitPageException is a clean way for quiting a page
			pass
		except:
			# Arbitrary other exception
			_stdout.delete_stdout()
			raise

		# Delete output stream
		_stdout.delete_stdout()

		# When in xslt-translate mode:
		if self.__server.output_mode in ('xhtml', 'atom'):
			content = stream.retrieve_data()

			# Apply filters
			content = self.__postprocess(content)

			# Content-type
			if self.__server.output_mode == 'xhtml':
				if (self.__server.request_headers.has_key('Accept') and
						self.__server.request_headers['Accept'].find('application/xhtml+xml') >= 0):

					self.__server.headers['Content-Type'] = 'application/xhtml+xml; charset=utf-8'
				else:
					self.__server.headers['Content-Type'] = 'text/html; charset=utf-8'

			elif self.__server.output_mode == 'atom':
					# self.__server.headers['Content-Type'] = 'application/atom+xml; charset=utf-8'
						# NOTE: not according to the rules, but FF doesn't accept the other
					self.__server.headers['Content-Type'] = 'text/xml; charset=utf-8'

			# Output headers
			self.__server.send_headers()

			# Output content
			output_buffer.write(content)


		elif self.__server.output_mode == 'raw':
			# Send headers if this hasn't been done before
			if not self.__server.headers_have_been_send():
				self.__server.send_headers()

	def __postprocess(self, content):
		"""
		Apply all the requested filters
		"""
		assert(isinstance(content, str))

		xhtml = (
			'<?xml version="1.0" encoding="utf-8"?>\n' +
			'<pp:webpage xmlns="http://www.w3.org/1999/xhtml" ' +
				'xmlns:pp="urn:python-pages-xml">' +
				content +
			'</pp:webpage>\n')

		from xml.parsers.expat import ExpatError

		# Try to parse result
		try:
			dom = xml.dom.minidom.parseString(xhtml)
		except ExpatError, e:
			raise PostProcessException(content, e.args[0], e.lineno, e.offset)

		# Be sure this document has a HTML/FEED node
		if self.__server.output_mode == 'xhtml':
			if len(dom.getElementsByTagName('html')) == 0:
				raise PostProcessException(content, 'No HTML node found', 1, 1)

		elif self.__server.output_mode == 'atom':
			if len(dom.getElementsByTagName('feed')) == 0:
				raise PostProcessException(content, 'No FEED node found', 1, 1)

		# Call filters
		for f, parameters in self.__server.get_filters():
			try:
				filter = getattr(__import__('filters.%s' % f), f)
				filter = getattr(filter, 'execute_minidom')
				filter(self.__server, dom, parameters)
			except ImportError, e:
				raise FilterException('Cannot load filter %s' % f)

		# Drop all pp nodes
		def drop_pp_nodes (newdom, newparent, node):
			if node.nodeType == node.ELEMENT_NODE:
				if node.prefix == 'pp':
					for c in node.childNodes:
						drop_pp_nodes(newdom, newparent, c)
				else:
					clone = node.cloneNode(False)
					newparent.appendChild(clone)
					for c in node.childNodes:
						drop_pp_nodes(newdom, clone, c)
			else:
				newparent.appendChild(node.cloneNode(False))

		if self.__server.output_mode == 'xhtml':
			newdom = xml.dom.minidom.parseString(
				'<?xml version="1.0" encoding="utf-8"?>\n' +
				'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n' +
				'<html xmlns="http://www.w3.org/1999/xhtml" />')

			for c in dom.getElementsByTagName('html')[0].childNodes:
				drop_pp_nodes(newdom, newdom.getElementsByTagName('html')[0], c)

		if self.__server.output_mode == 'atom':
			newdom = xml.dom.minidom.parseString(
				'<?xml version="1.0" encoding="utf-8"?>\n' +
				'<feed xmlns="http://www.w3.org/2005/Atom" />')

			for c in dom.getElementsByTagName('feed')[0].childNodes:
				drop_pp_nodes(newdom, newdom.getElementsByTagName('feed')[0], c)


		# Set doctype
		## doctype = xml.dom.minidom.DocumentType('html')
		## doctype.publicId = "-//W3C//DTD XHTML 1.0 Strict//EN"
		## doctype.systemId = "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"
		## newdom.doctype = doctype

		# Return processed content
		return newdom.toxml(encoding="utf-8")

class PostProcessException(Exception):
	def __init__(self, xhtml_output, error_message, lineno, offset):
		self.error_message = error_message
		self.xhtml_output = xhtml_output
		self.lineno = lineno
		self.offset = offset

class AccessDeniedException(Exception):
	def __init__(self, message):
		Exception.__init__(self, message)

class RenderException(Exception):
	def __init__(self, message):
		Exception.__init__(self, message)

class OutputBufferException(Exception):
	def __init__(self, message):
		Exception.__init__(self, 'Output buffer exception: %s' % message)

class FilterException(Exception):
	def __init__(self, message):
		Exception.__init__(self, message)
